import { Container, Service } from 'typedi';
import { CreateMenuDto, MenuPageDto, UpdateMenuDto } from '@/dtos';
import { IMenu, IMenuPageResult } from '@/interfaces';
import { PrismaClient, MenuType } from '@/generated/prisma';
import Redis from 'ioredis';
import { BadRequestException, MenuExistedException, MenuNotFoundException } from '@/exceptions';

@Service()
export class MenuService {
  public token_redis: Redis = Container.get('REDIS_TOKEN');
  public online_redis: Redis = Container.get('REDIS_ONLINE');
  public prisma: PrismaClient = Container.get('PRISMA_CLIENT');

  async getMenuTree(): Promise<IMenu[]> {
    return this.prisma.menu.findMany();
  }

  async getMenuTreeByRoles(roles: string[]): Promise<IMenu[]> {
    if (!roles.length) return [];
    const roleIds = await this.prisma.role
      .findMany({
        where: { code: { in: roles } },
        select: { id: true },
      })
      .then(list => list.map(r => r.id));
    if (!roleIds.length) return [];

    const menuIds = await this.prisma.roleMenu
      .findMany({ where: { roleId: { in: roleIds } }, select: { menuId: true } })
      .then(list => [...new Set(list.map(m => m.menuId))]);

    return this.prisma.menu.findMany({
      where: {
        id: { in: menuIds },
        type: { in: [MenuType.directory, MenuType.menu] },
        status: true,
      },
      orderBy: { sort: 'asc' },
    });
  }

  async getButtonPermCodesByRoles(roles: string[]): Promise<string[]> {
    if (!roles.length) return [];
    const roleIds = await this.prisma.role
      .findMany({
        where: { code: { in: roles } },
        select: { id: true },
      })
      .then(list => list.map(r => r.id));
    if (!roleIds.length) return [];
    const menus = await this.prisma.menu.findMany({
      where: {
        type: MenuType.button,
        status: true,
        roles: { some: { roleId: { in: roleIds } } },
      },
      select: { permCode: true },
    });
    return menus.map(m => m.permCode).filter((c): c is string => !!c);
  }

  async getApiPermCodesByRoles(roles: string[]): Promise<string[]> {
    if (!roles.length) return [];
    const roleIds = await this.prisma.role
      .findMany({
        where: { code: { in: roles } },
        select: { id: true },
      })
      .then(list => list.map(r => r.id));
    if (!roleIds.length) return [];

    const menus = await this.prisma.menu.findMany({
      where: {
        type: MenuType.api,
        status: true,
        roles: { some: { roleId: { in: roleIds } } },
      },
      select: { permCode: true },
    });
    return menus.map(m => m.permCode).filter((c): c is string => !!c);
  }

  async findAll(dto: MenuPageDto): Promise<IMenuPageResult> {
    const where = {
      ...(dto.name && { name: { contains: dto.name } }),
      ...(dto.title && { title: { contains: dto.title } }),
      ...(dto.status !== undefined && { status: dto.status }),
      parentId: null,
    };

    const total = await this.prisma.menu.count({ where });
    if (total === 0) return { total, list: [] };

    const roots = await this.prisma.menu.findMany({
      where,
      orderBy: { id: 'asc' },
      skip: dto.offset,
      take: dto.pageSize,
      select: { id: true },
    });
    const rootIds = roots.map(r => r.id);
    const allIds = await this.getAllChildrenIds(rootIds);

    const flatList = await this.prisma.menu.findMany({
      where: { id: { in: allIds } },
      include: {
        parent: {
          select: { name: true },
        },
      },
      orderBy: [{ sort: 'asc' }, { id: 'asc' }],
    });

    const result = flatList.map(item => {
      const { parent, ...content } = item;
      return {
        ...content,
        ...(parent && { parentName: parent.name }),
      };
    });

    return { list: result, total };
  }

  async findOne(id: number) {
    const menu = await this.prisma.menu.findUnique({ where: { id } });
    if (!menu) throw new MenuNotFoundException();
    return menu;
  }

  async create(dto: CreateMenuDto) {
    const exist = await this.prisma.menu.findFirst({
      where: { name: dto.name, parentId: dto.parentId ?? null },
    });
    if (exist) throw new MenuExistedException('同父级下名称已存在');

    if (dto.parentId) {
      const parent = await this.prisma.menu.findUnique({ where: { id: dto.parentId } });
      if (!parent) throw new MenuNotFoundException('父菜单不存在');
    }

    await this.prisma.menu.create({
      data: {
        ...dto,
        permCode: dto.name.toLowerCase(),
      },
    });
  }

  async update(id: number, dto: UpdateMenuDto) {
    const exist = await this.findOne(id);
    if (!exist) throw new MenuNotFoundException();

    // 名称冲突检测
    if (dto.name || dto.parentId !== undefined) {
      const nameExist = await this.prisma.menu.findFirst({
        where: {
          name: dto.name ?? exist.name,
          parentId: dto.parentId ?? exist.parentId,
          NOT: { id },
        },
      });
      if (nameExist) throw new MenuExistedException('同父级下名称已存在');
    }

    // 环形引用检测
    if (dto.parentId && dto.parentId !== exist.parentId) {
      const descIds = await this.getAllChildrenIds([id]);
      if (descIds.includes(dto.parentId)) throw new BadRequestException('不能将自身或子孙设为父级');
    }

    await this.prisma.menu.update({
      where: { id },
      data: {
        ...dto,
        permCode: dto.name.toLowerCase(),
      },
    });
  }

  async remove(id: number) {
    const menu = await this.findOne(id);
    if (!menu) throw new MenuNotFoundException();
    await this.prisma.menu.delete({ where: { id } });
  }

  private async getAllChildrenIds(parentIds: number[]): Promise<number[]> {
    const allIds = new Set<number>(parentIds);
    const collectIds = async (currentIds: number[]): Promise<void> => {
      if (currentIds.length === 0) return;
      const children = await this.prisma.menu.findMany({
        where: { parentId: { in: currentIds } },
        select: { id: true },
      });
      const childIds = children.map(c => c.id);
      childIds.forEach(id => allIds.add(id));
      await collectIds(childIds);
    };
    await collectIds(parentIds);
    return Array.from(allIds);
  }
}
